# coding=utf-8
import logging
import os
import threading
import time

import pytest
from jsonpath import jsonpath
from common.db import *
from common.logger import Logger

class Utils:
    def get_user_guid_by_code(self,user_code):
        """
        通过用户code，获取用户guid
        :param user_code:
        :return:
        """
        id = sqlserver_query(f"SELECT id FROM dbo.p_user WHERE code='{user_code}'")
        if id==[]:
            user_guid = ''
        else:
            user_guid = id[0][0]
        return user_guid

    def get_column_value_by_two_array(self, two_array, field_name):
        """
        根据包含列名的二维数据，获取列的值。如：[['序号', '步骤名称', '节点序号', '流程GUID', '流程名称', '责任人', '节点状态', '步骤状态', '流程状态', '节点GUID', '用户code'], ['000', '开始', 'None', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '系统管理员', '4', '4', '7', 'b6d6df9d-9a1b-baea-7f59-39feccab196c', 'admin']
        :param two_array: 包含列名的二维数组，如：[['序号', '步骤名称', '节点序号', '流程GUID', '流程名称', '责任人', '节点状态', '步骤状态', '流程状态', '节点GUID', '用户code'], ['000', '开始', 'None', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '系统管理员', '4', '4', '7', 'b6d6df9d-9a1b-baea-7f59-39feccab196c', 'admin']
        :param field_name: 需要获取的值的列
        :return: 返回列的值，是个列表
        """
        try:
            index = two_array[0].index(field_name)
        except ValueError:
            assert False,f"列名【{field_name}】不存在！已有的列名为【{two_array[0]}】"
        # 二维数组中，列名组不取值，所以从[1:]开始，如果只有列名，没有数据，则会返回False
        target_list = jsonpath.jsonpath(two_array,f"$[1:][{index}]")
        Logger.info(f"列【{field_name}】的值：【{target_list}】")
        return target_list

    def query_until_all_matched(self,query_sql,expect,timeout=5,query_alias='',by_length=False):
        """
        查询结果直到等于预期值,需要查询结果和预期值的顺序必须一致，示例：utils.query_until_all_matched(history_sql, expect['数据库校验'])
        :param query_sql:   查询sql
        :param expect:  预期值，一般直接取yml里面的数据库校验的预期结果
        :param timeout: 超时时间
        :param query_alias: 查询主题，方便搜索预期结果
        :param by_length: 消息迁移最长需要5s，使用消息查询结果使用消息个数进行判断是否查询成功
        :return:
        """
        query_result = sqlserver_query_return_contain_field(query_sql,query_alias)
        while_condition = False
        times = 0
        while while_condition == False:
            if by_length:    # 如果使用消息条数来做匹配校验，则进行len()转换成长度
                while_condition = len(expect) == len(query_result)
            else:   # 否则通过完整查询结果是否匹配，作为循环条件
                while_condition = expect == query_result    # 如果查询结果与预期相等，while_condition=True
            if while_condition: #相等则跳出并返回查询结果
                return query_result
            query_result = sqlserver_query_return_contain_field(query_sql,query_alias)
            time.sleep(1)
            times += 1
            if times > timeout:
                Logger.error(f'等待超时。等于失败！第{times}次执行查询：\n{query_sql}\n查询的实际值是\n{query_result}\n与预期值\n{expect}\n不匹配')
                return query_result


    def query_until_column_value_matched(self, query_sql, column_name, condition, column_value, index = 0):
        """
        执行sql查询，使用包含字段名的查询sql，等待字段值与条件值匹配，并返回查询结果。查询10秒。示例：utils.query_until_column_value_matched(sql, '步骤名称','!=','分流')
        :param query_sql: 待执行的sql
        :param column_name: 需要对比的字段名
        :param condition: 对比条件：eq/==(左边等于右边)、ne/!=(左边不等于右边)、contains(左边包含右边)、included(左边被包含于右边)
        :param column_value: 需要对比的字段值，如果是数值，也传成字符串
        :param index: 根据字段名获取到的字段值是列表，index取列表值
        :return:
        """
        query_result = sqlserver_query_return_contain_field(query_sql)
        result = self.get_column_value_by_two_array(query_result, column_name)
        # 获取列表还是获取值进行对比，默认取列表第0列的值，获取值进行对比。需要判断列值是否存在，不存在result直接为False
        if isinstance(index,int) and result != False:
            result = result[index]
        while_condition = False
        times = 0
        while while_condition == False:
            # 根据条件，确定使用判断方法，eq表示等于
            if condition.lower() in ['eq' ,'==']:
                while_condition = result == column_value
                # print(f'while_condition1:{while_condition}')
                # print(f'result1:{result}')
                # print(f'column_value1:{column_value}')
            # ne表示查询结果 不等于入参column_value
            elif condition.lower() in ['ne','!=']:
                while_condition = result != column_value
            # contains表示查询结果 包含入参column_value
            elif condition.lower() == 'contains':
                while_condition = column_value in result
            # included表示查询结果被 包含在入参column_value中
            elif condition.lower() == 'included':
                while_condition = result in column_value
            else:
                assert False, f'【{condition}】条件判断方式不存在！'
            Logger.info(f'循环查询结果：【{result}】')
            # 如果查询结果与入参的判断是True，则跳出循环查询
            if while_condition:
                return query_result
                # break
            time.sleep(1)
            # 重复查询
            query_result = sqlserver_query_return_contain_field(query_sql)
            result = self.get_column_value_by_two_array(query_result, column_name)
            # 获取列表还是获取值进行对比，默认取列表第0列的值，获取值进行对比。需要判断列值是否存在，不存在result直接为False
            if isinstance(index, int) and result != False:
                result = result[index]
            Logger.info(f'第{times + 2}次执行查询：【{query_sql}】')
            times += 1
            # print(f'while_condition2:{while_condition}')
            # print(f'result2:{result}')
            # print(f'column_value2:{column_value}')
            if times > 10:
                if condition in ['eq' ,'==']:
                    assert False, f'等待超时。等于失败！第{times}次执行查询：【{column_name}】的实际值是【{result}】，与预期值【{column_name}】不匹配'
                elif condition in ['ne' ,'!=']:
                    assert False, f'等待超时。不等于失败！第{times}次执行查询：【{column_name}】的实际值是【{result}】，与预期值【{column_name}】匹配'
                elif condition == 'contains':
                    assert False, f'等待超时。包含失败！第{times}次执行查询：【{column_name}】的实际值是【{result}】，不包含预期值【{column_name}】'
                elif condition == 'included':
                    assert False, f'等待超时。被包含失败！第{times}次执行查询：【{column_name}】的实际值是【{result}】，不在预期值【{column_name}】内'

    def concurrent_execute(self,func_list, params_list):
        """
        用于测试并发请求的方法，func_list为函数列表，params_list为对应函数的参数，必须为完整参数。
        params = (param['user'], param['ProcessModuleName'], param['BusinessGUID'], '', param['OtherParams'])
        func_list = [main.AutoInitiateProcess_By_Module, main.AutoInitiateProcess_By_Module]
        params_list = [params, params]
        utils.concurrent_execute(func_list,params_list)
        以上为调用示例。
        :param func_list: 函数列表
        :param params_list: 函数对应参数列表，列表内是tuple格式作为单个函数入参
        :return:
        """
        threads = []
        for index, params in enumerate(params_list):
            t = threading.Thread(target=func_list[index], args=params)
            # 线程组
            threads.append(t)
        for t in threads:
            t.start()
        for t in threads:
            t.join()

    def pack_confirm_steps_field(self, response_json, other_actuals='', verify_alias=''):
        """
        确认路径响应json，组装需要校验的重要值
        :param response_json:   确认路径响应
        :param other_actuals: 其他actuals下要校验的字段，传入actuals下的路径如：['.stepAttrs.auto_pass_rules','.tag']
        :param verify_alias: 校验别名，如：确认路径校验、选择分支校验
        :return:
        """
        steps = jsonpath.jsonpath(response_json,'$.data.actuals[*].description')    # 必须使用description，如果使用name，分流步骤显示分流，
        try:
            actuals = jsonpath.jsonpath(response_json,f'$.data.actuals')[0]
        except:
            assert False,f'jsonpath：【$.data.actuals】从{response_json}获取元素失败'
        verify_steps = {}
        # 固定校验步骤的字段，在actuals下的各个字段的jsonpath
        field = ['.stepAttrs.handle_mode', '.stepAttrs.multi_type', '.stepAttrs.adjust_auditor_order',
                 '.stepAttrs.adjust_auditor', '.stepAttrs.no_auditor_rules','.stepAttrs.archived_mode',
                 '.stepAttrs.adjust_multi_auditor','.stepAttrs.auto_pass_rules','.needChoose',
                 '.needChooseBranch','.step_type','.rollBackType']
        # 需要校验步骤中的其他字段时，其他actuals下要校验的字段，传入actuals下的路径如：['.stepAttrs.auto_pass_rules','.tag']
        if other_actuals != '':
            field.extend(other_actuals)
        for step in steps:
            try:
                verify_step = {}
                # 获取每个步骤对应jsonpath的值，进行组装。
                for tags in field:
                    # 例如：handle_mode = jsonpath.jsonpath(actuals,f'$[?(@.description=="{step}")].stepAttrs.handle_mode')[0]
                    value = jsonpath.jsonpath(actuals, f'$[?(@.description=="{step}")]{tags}')[0]
                    key = tags.split('.')[-1]   # 如：.stepAttrs.handle_mode，切分后使用handle_mode作为key
                    verify_step[key] = value
                # 节点处理人单独处理
                nodes = jsonpath.jsonpath(actuals,f'$[?(@.description=="{step}")].nodes')[0]
                # 去掉不校验的guid
                for node in nodes:
                    for k,v in node.items():    # 替换None的值，改为'None'
                        if v == None: node[k] = 'None'
                    node.pop('auditor_id')
                    node.pop('station_guid')
                verify_step['nodes'] = nodes
                # 将存在None的值，改为'None'，否则直接复制到yml后，校验始终会失败
                for key,value in verify_step.items():
                    if value == None:
                        verify_step[key] = 'None'
                # 所有明确步骤添加到待校验的字典中
                verify_steps[step] = verify_step
            except Exception as e:
                assert False, f'需校验的字段不存在:{repr(e)}'
        # 获取分支步骤名，校验分支
        branch = jsonpath.jsonpath(response_json,f'$.data.branch[*].description')
        if not branch:  # jsonpath获取不到分支步骤，给个默认空列表
            branch=[]
        verify_steps['branch'] = branch
        # 获取校验结果值
        verify_steps['validateResult'] = jsonpath.jsonpath(response_json,f'$.data.validateResult')[0]
        if verify_alias=='':
            name = '确认路径校验'
        else:
            name = verify_alias
        Logger.debug(f'****{name}****：\n{verify_steps}')
        # print(f'****{name}****：\n{verify_steps}')
        return verify_steps

    def update_json_by_jsonpath(self,json_obj, jsonpath_expr, value):
        """
        通过jsonpath表达式，来更新指定json的key值，jsonpath必须唯一匹配到一个key
        :param json_obj:    原始json对象
        :param jsonpath_expr:   需要更新的位置的jsonpath表达式
        :param value:   需要更新的值,正常传入json，字符串即可
        :return:    更新后的json
        """
        update_expr = 'json_obj'
        # path = jsonpath(json_name, '$.actual_steps[?(@.description=="结束")].nodes', 'IPATH')[0]
        IPATH = jsonpath.jsonpath(json_obj, jsonpath_expr, 'IPATH')
        if IPATH == False:
            assert False, f'jsonpath表达式：{jsonpath_expr}无法从{json_obj}中提取到元素'
        path = IPATH[0]
        # 组装字典表达式，最终形成如：json_obj['actual_steps'][1]['nodes']格式
        for index, key in enumerate(path):
            try:
                key = int(key)
            except ValueError:
                key = f"\'{key}\'"
            update_expr += f"[{key}]"
        # 将表达式赋值
        if type(value) in [dict,list,bool,int] or value is None:
            update_expr = f"{update_expr}={value}"
        else:
            update_expr = f"{update_expr}='{value}'"
        # print(f"更新表达式：{update_expr}")
        # 通过exec完成json更新。eval()函数只能计算单个表达式的值，而exec()函数可以动态运行代码段。eval()函数可以有返回值，而exec()函数返回值永远为None
        exec(update_expr)
        # print(f'更新后的json结果:{json_obj}')
        return json_obj

    def mark_skip_case(self,case_param, by_key):
        """
        通过对用例参数标记的skip对应的值，如果包含传入的by_key，则跳过用例。mark_skip_case(param,'bug')
        :param case_param: 用例param
        :param by_key: 标记的关键字，如bug
        :return:
        """
        try:
            skip_value = case_param['skip']    # 获取用例中skip的值。用于判断是否跳过用例
        except:
            skip_value = ''
        if by_key.lower() in skip_value.lower():    # 比如 'bug' in '延期解决bug，暂不执行用例'
            pytest.skip(skip_value)

    def restore(self,redis_ip,db,snapshot):
        """
        还原数据库并清理缓存，如restore('10.20.180.11','10.5.11.43\SQL2016','02还原快照_流程中心接口库快照还原.sql')
        :param redis_ip: redis服务ip：10.20.180.11
        :param db: 数据库服务器，如：10.5.11.43\SQL2016
        :param snapshot: 还原快照sql文件，需在configs/resource下，如：02还原快照_流程中心接口库快照还原.sql
        :return:
        """
        # 文件所在的绝对路径然后切分：D:\PycharmProjects\ProcessCenter_Api_Autotest
        abspath = os.path.abspath(__file__).split(r'\common\utils.py')[0]
        # restore_db = 'sqlcmd -S"10.5.11.43\SQL2016" -U"sa" -P"95938" -d"master" -i 02还原快照_流程中心接口库快照还原.sql'
        # clean_cache = 'redis-cli.exe -h 10.20.180.11 -p 6379 flushall'
        restore_db = f'sqlcmd -S"{db}" -U"sa" -P"95938" -d"master" -i {snapshot}'   # 还原数据库快照的命令
        clean_cache = f'redis-cli.exe -h {redis_ip} -p 6379 flushall'   # 清理redis缓存命令
        os.system(f'cd {abspath}/configs/resource &{restore_db}&{clean_cache}') # 切换到configs/resource下后，执行还原数据库，并清理缓存命令

if __name__ == "__main__":
    two_array = [['序号', '步骤名称', '节点序号', '流程GUID', '流程名称', '责任人', '节点状态', '步骤状态', '流程状态', '节点GUID', '用户code'], ['000', '开始', 'None', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '系统管理员', '4', '4', '7', 'b6d6df9d-9a1b-baea-7f59-39feccab196c', 'admin'], ['001', '审批步骤1_多人并行', '0', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '采购经理', '4', '4', '7', 'b312ff40-ffe8-4233-d64d-39feccab1a4a', 'cg02'], ['001', '审批步骤1_多人并行', '1', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '工程经理', '4', '4', '7', '60b2ea6e-7f73-a140-af53-39feccab1a4a', 'gc01'], ['002', '分流', 'None', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '', '4', '4', '7', '220bf9b8-93d4-a8fc-b906-39feccab1a4a', 'None'], ['003', '审批步骤2_单人分流步骤1', '0', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '采购经理', '32', '32', '7', '5b87804c-5435-525c-bddc-39feccab1a4a', 'cg02'], ['004', '合流', 'None', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '', '0', '0', '7', '339270d7-cc8b-3bc9-3600-39feccab1a4a', 'None'], ['005', '审批步骤3_多人并行', '0', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '采购经理', '0', '0', '7', 'a8097537-8153-5284-3ee8-39feccab1a4a', 'cg02'], ['005', '审批步骤3_多人并行', '1', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '工程经理', '0', '0', '7', 'c277e085-a9fe-6d98-0694-39feccab1a4a', 'gc01'], ['006', '审批步骤2_单人串行分流步骤2', '0', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '工程经理', '10', '10', '7', 'f9e4a874-e036-f130-7286-39feccab1a4a', 'gc01'], ['006', '审批步骤2_单人串行分流步骤2', '1', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '销售经理', '0', '10', '7', '9fd2efca-c929-828d-4c6c-39feccab1a4a', 'yx01'], ['007', '审批步骤2_单人并行分流步骤3', '0', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '工程经理', '32', '32', '7', '5d2eb74f-34c6-020c-07c2-39feccab1a4a', 'gc01'], ['007', '审批步骤2_单人并行分流步骤3', '1', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '销售经理', '32', '32', '7', 'a77b1905-8641-77ea-b902-39feccab1a4a', 'yx01'], ['008', '结束', 'None', '228103ac-d478-bed5-6411-39feccab196b', '01外部接口使用非业务流程-集团-2021-09-06-0002', '', '0', '0', '7', '33105e69-254e-e2e4-dcba-39feccab1a4a', 'None']]
    # two_array = [['序号', '步骤名称', '节点序号', '流程GUID', '流程名称', '责任人', '节点状态', '步骤状态', '流程状态', '节点GUID', '用户code'], ]
    get_field_value = Utils().get_column_value_by_two_array(two_array, '责任人')
    # get_field_value = Utils().get_user_guid_by_code('admin')
    # print(get_field_value)

